TypeScript 5.4: tipos más potentes, menos trucos

Fragmentos de código TypeScript con anotaciones de tipo en colores

TypeScript 5.4 se publicó el 6 de marzo de 2024 y, leída con perspectiva, encaja en esa categoría de releases que se describen como “incrementales” pero cuyo impacto real en el día a día es desproporcionado. No introduce un cambio de paradigma comparable a satisfies o a los template literal types, pero sí retira tres o cuatro fricciones que llevaban años presentes en cualquier código base TypeScript medianamente genérico. La lectura honesta es que hay dos o tres features que vas a usar esta misma semana, una mejora de rendimiento que solo se nota en monorepos, y un puñado de añadidos que son útiles si escribes librerías pero irrelevantes si consumes tipos ajenos.

NoInfer: el arreglo que llevábamos años pidiendo

La estrella de la release es el utility type NoInfer<T>. Resuelve un problema específico pero omnipresente: cuando una función genérica toma varios parámetros que mencionan el mismo type parameter, TypeScript intenta inferirlo desde todos ellos, ensanchando el tipo resultante más allá de lo que el autor pretendía.

El caso canónico es una función que recibe una lista de valores permitidos y un valor por defecto. Si el segundo parámetro participa en la inferencia, puedes pasar un literal que no pertenece al conjunto y TypeScript lo aceptará alegremente, porque el propio acto de pasarlo ensancha C hasta incluirlo. Antes de 5.4 la solución pasaba por dos type parameters separados, o por un conditional type que forzaba distribución — un workaround legible solo para quien ya había visto el patrón.

function createStreetLight<C extends string>(
  colors: C[],
  defaultColor?: NoInfer<C>
) {}

createStreetLight(["red", "yellow", "green"], "blue");
// Error: '"blue"' no es asignable a '"red" | "yellow" | "green"'

function withDefault<T>(value: T, fallback: NoInfer<T>): T {
  return value ?? fallback;
}
withDefault(42, 0);            // T = number, ok
withDefault("hi", 42);         // Error — T fijado a string

Los sitios donde NoInfer rinde de forma clara son callbacks que reciben valores ya inferidos, parámetros de defaults, options bags donde el tipo principal se deduce de otro argumento, y builders fluidos donde la forma del objeto se establece en el primer método. Los autores de Zod, tRPC y React Query lo incorporan en sus próximas versiones mayores, así que indirectamente llegará a cualquier proyecto moderno aunque no lo uses explícitamente.

La guía de estilo personal que recomiendo: úsalo cuando hay un parámetro que es claramente la fuente de verdad y otros que solo lo consumen. Evítalo cuando ambos parámetros son simétricos o cuando un cast puntual resulta igual de legible. No está para demostrar dominio de utility types.

Narrowing preservado en closures síncronas

El segundo cambio que vas a notar esta semana es el narrowing preservado dentro de callbacks ejecutados de forma síncrona. Históricamente, un arr.every(x => typeof x === 'string') estrechaba el tipo de arr fuera del if, pero en cuanto entrabas en un forEach, map o cualquier método de array, TypeScript olvidaba ese narrowing porque el callback podría, en teoría, ser llamado más tarde con arr modificado. El compilador ahora reconoce que los métodos estándar de arrays se ejecutan síncronamente y mantiene el estrechamiento dentro del callback.

El resultado práctico es menos as string[] salpicados por el código, menos if (typeof x === 'string') defensivos redundantes, y menos variables intermedias creadas solo para capturar el tipo estrechado. Es de esas mejoras invisibles hasta que dejas de escribirlas.

Object.groupBy y Map.groupBy

TS 5.4 tipa los métodos Object.groupBy y Map.groupBy que en ese momento estaban en stage 3 de TC39. La firma devuelve Partial<Record<K, T[]>>, con el Partial marcando correctamente que no todas las claves del union aparecen necesariamente. Es una conveniencia directa que elimina una dependencia de lodash en proyectos que solo la usaban para esto. No es revolucionario, pero retira una excusa para tirar de librería.

Rendimiento del compilador

Las notas de release mencionan menor consumo de memoria, builds incrementales más rápidos y mejor deduplicación de type instances. La realidad es que en un proyecto de diez archivos no vas a notar nada. En un monorepo con cientos de paquetes y un tsc --build que tardaba minutos, la diferencia es perceptible — no transformadora, pero sí suficiente para que el CI respire. Si tu pain point principal era el tiempo de type-check, esta release no lo resuelve; mira ts-go o soluciones de build paralelo.

Lo que no trae y lo que puede romper

Los breaking changes son menores y puntuales: homomorfismo más estricto en mapped type variations puede afectar a librerías con type gymnastics agresivo, y algunas firmas de Function.prototype.bind se endurecen. En código de aplicación la probabilidad de notarlo es prácticamente cero.

Las ausencias son las de siempre: decorators estables completos, pipeline operator, pattern matching, branded types como construcción de lenguaje. Todas esperan movimientos previos en TC39 o diseños que aún no tienen consenso. Pedirlas a TypeScript es pedir a la parte equivocada de la cadena.

Actualizar desde 5.3

En la abrumadora mayoría de proyectos, la migración se reduce a npm install -D typescript@5.4, correr tsc --noEmit y comprobar si aparecen errores nuevos. Si usas @typescript-eslint, actualiza el parser a una versión compatible en el mismo commit para evitar un CI roto. Vite, esbuild, swc, Next, Nuxt, SvelteKit y el resto del stack moderno no requieren cambios. Es una de las releases menos traumáticas de los últimos años.

Conclusión

TypeScript 5.4 no cambia cómo piensas sobre tipos, pero retira tres workarounds que llevabas años escribiendo sin darte cuenta. NoInfer convierte en una anotación trivial lo que antes requería dos type parameters o un conditional type oscuro, y la mejora se propaga vía las librerías que todo el mundo usa. El narrowing preservado en closures elimina una categoría entera de casts molestos. Object.groupBy cierra la última excusa para importar lodash solo por agrupar una lista. El rendimiento ayuda donde antes dolía. Las features que faltan siguen faltando, pero eso es problema de TC39, no del equipo de TypeScript. Para un equipo activo, migrar pronto compensa: los patrones nuevos hacen el código más expresivo y los antiguos empiezan a oler a legacy más rápido de lo esperado.

Entradas relacionadas